package traefik_minio_hashfile

import (
	"context"
	"crypto/hmac"
	"crypto/md5"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
	"strings"
	"time"
)

type Config struct {
	Username       string   `json:"username,omitempty"`
	Secret         string   `json:"secret,omitempty"`
	Buckets        []string `json:"buckets,omitempty"`
	PrivateBuckets []string `json:"privateBuckets,omitempty"`
}

func CreateConfig() *Config {
	return &Config{}
}

type Minio struct {
	next           http.Handler
	username       string
	secret         string
	buckets        []string
	privateBuckets []string
}

func (m *Minio) Validate(url *url.URL) string {
	// 格式为/BUCKET/UID/SIGN/FILENAME，分成5段
	parts := strings.Split(url.Path, "/")
	if len(parts) != 5 {
		return ""
	}

	bucket, uid, sign, fileName := parts[1], parts[2], parts[3], parts[4]
	isPrivate := Contain(m.privateBuckets, bucket)

	if len(sign) <= 16 || !(Contain(m.buckets, bucket) || isPrivate) {
		return ""
	}

	if isPrivate {
		token := url.Query().Get("token")
		tparts := strings.Split(token, "_")
		if len(tparts) != 3 {
			return ""
		}
		tokenUID, expire, tokenSign := tparts[0], tparts[1], tparts[2]
		resignTokenOrigin := fmt.Sprintf("%s_%s_%s", tokenUID, expire, m.secret)
		resignToken := MD5(resignTokenOrigin)[:16]
		if tokenSign != resignToken {
			return ""
		}
		if tokenUID != "s" && tokenUID != uid {
			return ""
		}
		expireTime, _ := strconv.ParseInt(expire, 10, 64)
		now := time.Now().Unix()
		if now > expireTime {
			return ""
		}
	}

	fileHash := sign[8:]
	resignOrigin := fmt.Sprintf("%s_%s_%s_%s", fileName, m.secret, fileHash, uid)
	resign := MD5(resignOrigin)[:8]
	if resign != sign[:8] {
		return ""
	}

	realPath := fmt.Sprintf("/%s/%s/%s", bucket, fileHash[:2], fileHash)
	return realPath
}

func (m *Minio) MinioSign(path string) (string, string) {
	date := time.Now().Format(time.RFC1123Z)
	signString := fmt.Sprintf("GET\n\n\n%s\n%s", date, path)
	mac := hmac.New(sha1.New, []byte(m.secret))
	mac.Write([]byte(signString))
	signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
	authorization := fmt.Sprintf("AWS %s:%s", m.username, signature)
	return date, authorization
}

func (m *Minio) ServeHTTP(res http.ResponseWriter, req *http.Request) {
	realPath := m.Validate(req.URL)
	if realPath == "" {
		m.next.ServeHTTP(res, req)
		return
	}

	date, authorization := m.MinioSign(realPath)
	req.Header.Add("Date", date)
	req.Header.Add("Authorization", authorization)

	req.URL.RawPath = realPath
	req.URL.Path, _ = url.PathUnescape(req.URL.RawPath)
	req.RequestURI = req.URL.RequestURI()
	m.next.ServeHTTP(res, req)
}

func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
	return &Minio{
		next:           next,
		username:       config.Username,
		secret:         config.Secret,
		buckets:        config.Buckets,
		privateBuckets: config.PrivateBuckets,
	}, nil
}

func MD5(data string) string {
	hmd5 := md5.Sum([]byte(data))
	return fmt.Sprintf("%x", hmd5)
}

func Contain(arr interface{}, obj interface{}) bool {
	arrVal := reflect.ValueOf(arr)
	switch reflect.TypeOf(arr).Kind() {
	case reflect.Slice, reflect.Array:
		for i := 0; i < arrVal.Len(); i++ {
			if arrVal.Index(i).Interface() == obj {
				return true
			}
		}
	case reflect.Map:
		if arrVal.MapIndex(reflect.ValueOf(obj)).IsValid() {
			return true
		}
	}
	return false
}
